/*
 * PDLogger
 * 
 * Copyright (c) 2012, Zoujie Wu <yibn2008@gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 * 
 *   * Redistributions of source code must retain the above copyright notice, this 
 *     list of conditions and the following disclaimer.
 *   
 *   * Redistributions in binary form must reproduce the above copyright notice, 
 *     this list of conditions and the following disclaimer in the documentation 
 *     and/or other materials provided with the distribution.
 *   
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/* $Id$ */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "php.h"
#include "ext/standard/php_string.h"
#include "zend_compile.h"
#include "php_pdlogger.h"
#include "pdl_output.h"
#include "pdl_record.h"
#include "pdl_watch.h"

#define PDL_GET_FUNC_ARGS(args, arg_count, p) \
	do { \
		uint i; \
		MAKE_STD_ZVAL(args); \
		array_init_size(args, arg_count); \
		for (i = 0; i < arg_count; i ++) { \
			zval *element; \
			ALLOC_ZVAL(element); \
			*element = **((zval **) (p-(arg_count-i))); \
			zval_copy_ctor(element); \
			INIT_PZVAL(element); \
			zend_hash_next_index_insert(Z_ARRVAL_P(args), &element, sizeof(zval *), NULL); \
		} \
	} while (0)

/* replace zend original compile file function */
static zend_op_array* (*_old_compile_file) (zend_file_handle* file_handle, int type TSRMLS_DC);
static zend_op_array* pdl_compile_file(zend_file_handle* file_handle, int type TSRMLS_DC);

/* replace zend original execute and execute_internal function */
static void (*_old_execute) (zend_op_array *op_array TSRMLS_DC);
static void pdl_execute(zend_op_array *op_array TSRMLS_DC);

static void (*_old_execute_internal) (zend_execute_data *data, int ret TSRMLS_DC);
static void pdl_execute_internal(zend_execute_data *data, int ret TSRMLS_DC);

/* error callback replacement functions */
static void (*_old_error_cb)(int type, const char *error_filename, const uint error_lineno, const char *format, va_list args);
static void (*_new_error_cb)(int type, const char *error_filename, const uint error_lineno, const char *format, va_list args);
static void pdl_error_cb(int type, const char *error_filename, const uint error_lineno, const char *format, va_list args);

/**
 * compile file function to replace the original one
 */
static zend_op_array* pdl_compile_file(zend_file_handle* file_handle, int type TSRMLS_DC)
{
	zend_op_array	*ret;
	pdl_time_t		t;
	double			msec;
	char			*tmp_str;
	int				tmp_len;
	char			*msg_str;
	int				msg_len;
	zval			*data;

	if (PDL_G(watch) & PDL_W_LOAD) {
		t = pdl_get_curr_time();
	}

	/* call old compile file function */
	ret = _old_compile_file(file_handle, type TSRMLS_CC);

	if (PDL_G(watch) & PDL_W_LOAD) {
		/* set msg */
		msec = pdl_calc_interval(t) * 1000;
		php_basename(file_handle->filename, strlen(file_handle->filename), NULL, 0, &tmp_str, (size_t *) &tmp_len TSRMLS_CC);
		msg_len = spprintf(&msg_str, 0, PDL_MSG_LOAD_FILE, tmp_str, msec);
		efree(tmp_str);

		/* init and set data */
		tmp_len = spprintf(&tmp_str, 0, "File path: %s", file_handle->opened_path);
		MAKE_STD_ZVAL(data);
		ZVAL_STRINGL(data, tmp_str, tmp_len, 0);

		pdl_record(PDL_LV_INFO, ZEND_STRL(PDL_LABEL_LOAD), msg_str, msg_len, NULL, 0, data TSRMLS_CC);

		efree(msg_str);
	}

	return ret;
}

/**
 * callable function prototype:
 * mixed <callable>(string $class_name, string $func_name, double $spent, array $args, object $called_obj)
 * if return valus is true, the function execute info will be recorded by PDLogger itself
 */
static void pdl_invoke_watch_callback(pdl_wf_t *wf, double spent, zval *args, zval *this_ptr, const char *file, uint line TSRMLS_DC)
{
	zval	**params;
	zval	*retval = NULL;
	zval	*data;
	char	*msg;
	uint	msg_len;
	char	*tmp_str;
	int		i, is_sec = 0;

	MAKE_STD_ZVAL(retval);
	ZVAL_NULL(retval);

	if (wf->watch_cb) {
		params = (zval **) emalloc(sizeof(zval *) * 5);

		MAKE_STD_ZVAL(params[0]);
		if (wf->class_name) {
			ZVAL_STRING(params[0], wf->class_name, 1);
		} else {
			ZVAL_NULL(params[0]);
		}
		
		MAKE_STD_ZVAL(params[1]);
		ZVAL_STRING(params[1], wf->func_name, 1);
		
		MAKE_STD_ZVAL(params[2]);
		ZVAL_DOUBLE(params[2], spent);

		params[3] = args;
		Z_ADDREF_P(params[3]);
		
		if (!this_ptr) {
			MAKE_STD_ZVAL(params[4]);
			ZVAL_NULL(params[4]);
		} else {
			params[4] = this_ptr;
			Z_ADDREF_P(params[4]);
		}

		call_user_function(EG(function_table), NULL, wf->watch_cb, retval, 5, params TSRMLS_CC);

		for (i = 0; i < 5; i ++) {
			zval_ptr_dtor(&params[i]);
		}
		efree(params);
	}

	if (spent > 1) {
		is_sec = 1;
	} else {
		spent *= 1000;
	}
	msg_len = spprintf(&msg, 0, "Call %s%s%s() with args in %.3f %s(s), see detail for more info", 
		wf->class_name ? wf->class_name : "",
		wf->class_name ? "::" : "",
		wf->func_name,
		spent, is_sec ? "sec" : "msec");

	MAKE_STD_ZVAL(data);
	Z_TYPE_P(data) = IS_STRING;
	pdl_format_zval(&tmp_str, args, 0, 0, 0 TSRMLS_CC);
	Z_STRLEN_P(data) = spprintf(&Z_STRVAL_P(data), 0, "function args: %s", tmp_str);

	pdl_record(PDL_LV_INFO, ZEND_STRL(PDL_LABEL_WATCH), msg, msg_len, file, line, (Z_TYPE_P(retval) == IS_NULL) ? data : retval TSRMLS_CC);

	efree(msg);
	efree(tmp_str);
	zval_ptr_dtor(&data);
	if (retval) {	
		zval_ptr_dtor(&retval);
	}
}

/**
 * execute function to replace the original one
 */
static void pdl_execute(zend_op_array *op_array TSRMLS_DC)
{
	pdl_time_t	t;
	const char	*class_name = NULL;
	uint		class_name_len = 0;
	const char	*func_name;
	char		*key;
	uint		key_len;
	const char	*file;
	uint		line;
	int			dup;
	void		**p;
	pdl_wf_t	*wf;
	zval		*args;
	uint		arg_count = 0;
	zend_bool	func_exists = 0;
	zend_execute_data *ex = EG(current_execute_data);
	
	/* when execute include/require/eval, there's no function name */
	func_name = ex->function_state.function->common.function_name;
	if ((PDL_G(watch) & PDL_W_FUNC) && func_name) {
		if (EG(This)) {
			dup = zend_get_object_classname(EG(This), &class_name, &class_name_len TSRMLS_CC);
			key_len = strlen(func_name) + 2 + class_name_len;
			key = (char *) emalloc(sizeof(char) * (key_len + 1));
			snprintf(key, key_len+1, "%s::%s", class_name, func_name);
			if (!dup) {
				efree((char *) class_name);
			}
		} else {
			key_len = strlen(func_name);
			key = estrndup(func_name, key_len);
		}
		zend_str_tolower(key, key_len);

		if (zend_hash_find(&PDL_G(watch_funcs), key, key_len+1, (void **) &wf) == SUCCESS) {
			/* get file and line */
			file = zend_get_executed_filename(TSRMLS_C);
			line = zend_get_executed_lineno(TSRMLS_C);

			/* get args */
			p = ex->function_state.arguments;
			arg_count = (uint)(zend_uintptr_t) *p;
			PDL_GET_FUNC_ARGS(args, arg_count, p);

			t = pdl_get_curr_time();
			func_exists = 1;
		}

		efree(key);
	}

	_old_execute(op_array TSRMLS_CC);

	if (func_exists) {
		pdl_invoke_watch_callback(wf, pdl_calc_interval(t), args, EG(This), file, line TSRMLS_CC);
		zval_ptr_dtor(&args);
	}
}

/**
 * execute_internal function to replace the original one
 */
static void pdl_execute_internal(zend_execute_data *data, int ret TSRMLS_DC)
{
	pdl_time_t	t;
	const char	*class_name = NULL;
	uint		class_name_len = 0;
	const char	*func_name;
	char		*key;
	uint		key_len;
	const char	*file;
	uint		line;
	int			dup;
	void		**p;
	pdl_wf_t	*wf;
	zval		*args;
	uint		arg_count = 0;
	zend_bool	func_exists = 0;

	/* when execute include/require/eval, there's no function name */
	func_name = data->function_state.function->common.function_name;
	if ((PDL_G(watch) & PDL_W_FUNC) && func_name) {
		if (data->object) {
			dup = zend_get_object_classname(data->object, &class_name, &class_name_len TSRMLS_CC);
			key_len = strlen(func_name) + 2 + class_name_len;
			key = (char *) emalloc(sizeof(char) * (key_len + 1));
			snprintf(key, key_len+1, "%s::%s", class_name, func_name);
			if (!dup) {
				efree((char *) class_name);
			}
		} else {
			key_len = strlen(func_name);
			key = estrndup(func_name, key_len);
		}
		zend_str_tolower(key, key_len);

		if (zend_hash_find(&PDL_G(watch_funcs), key, key_len+1, (void **) &wf) == SUCCESS) {
			/* get file and line */
			file = zend_get_executed_filename(TSRMLS_C);
			line = zend_get_executed_lineno(TSRMLS_C);

			/* get args */
			p = data->function_state.arguments;
			arg_count = (uint)(zend_uintptr_t) *p;
			PDL_GET_FUNC_ARGS(args, arg_count, p);

			t = pdl_get_curr_time();
			func_exists = 1;
		}

		efree(key);
	}

	if (_old_execute_internal) {
		_old_execute_internal(data, ret TSRMLS_CC);
	} else {
		execute_internal(data, ret TSRMLS_CC);
	}

	if (func_exists) {
		pdl_invoke_watch_callback(wf, pdl_calc_interval(t), args, EG(This), file, line TSRMLS_CC);
		zval_ptr_dtor(&args);
	}
}

/**
 * get php error type name
 */
static const char* pdl_error_type_name(int type)
{
	switch (type) {
		case E_ERROR: return "E_ERROR";
		case E_CORE_ERROR: return "E_CORE_ERROR";
		case E_COMPILE_ERROR: return "COMPILE_ERROR";
		case E_USER_ERROR: return "E_USER_ERROR";
#if PHP_VERSION_ID >= 50200
		case E_RECOVERABLE_ERROR: return "E_RECOVERABLE_ERROR";
#endif
		case E_WARNING: return "E_WARNING";
		case E_CORE_WARNING: return "E_CORE_WARNING";
		case E_COMPILE_WARNING: return "E_COMPILE_WARNING";
		case E_USER_WARNING: return "E_USER_WARNING";
		case E_PARSE: return "E_PARSE";
		case E_NOTICE: return "E_NOTICE";
		case E_USER_NOTICE: return "E_USER_NOTICE";
		case E_STRICT: return "E_STRICT";
#if PHP_VERSION_ID >= 50300
		case E_DEPRECATED: return "E_DEPRECATED";
		case E_USER_DEPRECATED: return "E_USER_DEPRECATED";
#endif
	}

	return "Unknown";
}

/**
 * callback of php error handling
 */
static void pdl_error_cb(int type, const char *file, const uint line, const char *format, va_list args)
{
	zval		*data = NULL;
	char		*buffer;
	int			level, buffer_len;
	const char	*type_str;
	char		*msg_str;
	int			msg_len;

	TSRMLS_FETCH();

	/* if watch error */
	if (PDL_G(watch) & PDL_W_ERROR) {
		/* level */
		switch (type) {
			case E_ERROR:
			case E_CORE_ERROR:
			case E_COMPILE_ERROR:
			case E_USER_ERROR:
#if PHP_VERSION_ID >= 50200
			case E_RECOVERABLE_ERROR:
#endif
			case E_PARSE:
				level = PDL_LV_ERROR;
				break;
			case E_WARNING:
			case E_CORE_WARNING:
			case E_COMPILE_WARNING:
			case E_USER_WARNING:
			case E_NOTICE:
			case E_USER_NOTICE:
			case E_STRICT:
#if PHP_VERSION_ID >= 50300
			case E_DEPRECATED:
			case E_USER_DEPRECATED:
#endif
				level = PDL_LV_WARNING;
				break;
			default:
				level = PDL_LV_DEBUG;
		}

		/* msg */
		type_str = pdl_error_type_name(type);
		msg_len = sizeof(PDL_MSG_WATCH_ERROR) - 2 + strlen(type_str);
		msg_str = (char *) emalloc(sizeof(char) * msg_len);
		sprintf(msg_str, PDL_MSG_WATCH_ERROR, type_str);
		msg_str[msg_len - 1] = 0;

		/* data */
		buffer_len = vspprintf(&buffer, PG(log_errors_max_len), format, args);

		MAKE_STD_ZVAL(data);
		ZVAL_STRING(data, buffer, 0);

		/* record php errors */
		pdl_record(level, ZEND_STRL(PDL_LABEL_WATCH), msg_str, msg_len, file, (uint) line, data TSRMLS_CC);

		efree(msg_str);
		/* needn't to free buffer */
		zval_ptr_dtor(&data);
	}

	_old_error_cb(type, file, line, format, args);
}

/**
 * register watch hooks
 */
PDL_API void pdl_register_watch_hooks(TSRMLS_D)
{
	/* register hooks */
	_old_compile_file = zend_compile_file;
	zend_compile_file = pdl_compile_file;

	_old_execute = zend_execute;
	zend_execute = pdl_execute;

	_old_execute_internal = zend_execute_internal;
	zend_execute_internal = pdl_execute_internal;

	_old_error_cb = zend_error_cb;
	zend_error_cb = pdl_error_cb;
}

/**
 * unregister watch hooks
 */
PDL_API void pdl_unregister_watch_hooks(TSRMLS_D)
{
	zend_compile_file = _old_compile_file;
	zend_execute = _old_execute;
	zend_execute_internal = _old_execute_internal;
	zend_error_cb = _old_error_cb;
}

/**
 * get watch status
 */
PDL_API pdl_record_t* pdl_get_watch_status(TSRMLS_D)
{
	pdl_record_t	*record;
	zval			*data = NULL;
	char			*tpl;
	const char		*mem_unit;
	const char		*time_unit;
	char			*tmp_str;
	uint			tmp_len;
	double			mem_usage;
	double			peak_usage;
	double			time_spent;

	/* memory usage */
	mem_usage = (double) zend_memory_usage(0 TSRMLS_CC);
	peak_usage = (double) zend_memory_peak_usage(0 TSRMLS_CC);

	if (mem_usage / BYTES_IN_MB < 1) {
		mem_usage /= BYTES_IN_KB;
		peak_usage /= BYTES_IN_KB;
		mem_unit = "KB";
	} else {
		mem_usage /= BYTES_IN_MB;
		peak_usage /= BYTES_IN_MB;
		mem_unit = "MB";
	}

	/* time spent */
	time_spent = pdl_calc_interval(PDL_G(start_time));
	if (time_spent < 1) {
		time_spent *= 1000;
		time_unit = "msec";
	} else {
		time_unit = "sec";
	}

	/* make data zval */
	tpl = "- memory usage: %.3f %s (peak %.3f %s)\n"
		  "- time spent: %.3f %s(s)\n"
		  "- loaded files: %d";

	tmp_len = spprintf(&tmp_str, 0, tpl, mem_usage, mem_unit, peak_usage, mem_unit, time_spent, time_unit, EG(included_files).nNumOfElements);
	MAKE_STD_ZVAL(data);
	ZVAL_STRINGL(data, tmp_str, tmp_len, 0);

	record = (pdl_record_t *) emalloc(sizeof(pdl_record_t));
	record->level 		= PDL_LV_INFO;
	record->time		= pdl_get_curr_time();
	record->label 		= estrndup(PDL_LABEL_STATUS, sizeof(PDL_LABEL_STATUS)-1);
	record->msg			= estrndup(PDL_MSG_STATUS, sizeof(PDL_MSG_STATUS)-1);
	record->data 		= data;
	record->file 		= estrndup(PDL_FILE_UNKNOWN, sizeof(PDL_FILE_UNKNOWN)-1);
	record->line 		= 0;

	return record;
}

/* {{{ proto int pdl_watch([int $items])
 */
PHP_FUNCTION(pdl_watch)
{
	long items;
	int	 argc = ZEND_NUM_ARGS();

	RETVAL_LONG((long) PDL_G(watch));

	if (zend_parse_parameters(argc TSRMLS_CC, "|l", &items) == FAILURE) {
		return;
	}

	if (argc > 0) {
		PDL_G(watch) = items;
	}
}
/* }}} */

#define IS_azAZ09_CHAR(c) ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_')

static int pdl_parse_watch_func_name(const char* func_str, uint str_len, char **class_name, char **func_name)
{
	const char *p = func_str;
	int  i;		/* must be signed */
	int  fpos = -1;

	if (str_len == 0 || (p[0] >= '0' && p[0] <= '9')) {
		return FAILURE;
	}

	for (i = str_len-1; i >= 0; i --) {
		/* check if func_str is legal */
		if (!IS_azAZ09_CHAR(p[i])) {
			if (fpos < 0 && i > 0 && p[i] == ':' && p[i-1] == ':') {
				/* func_len > 0 */
				if (i < str_len-1) {
					fpos = i + 1;
					if (p[fpos] >= '0' && p[fpos] <= '9') {
						return FAILURE;
					}
					i --;
					continue;
				}
			}
			return FAILURE;
		}
	}
	/* if there's no special chars [^a-zA-Z0-9_] */
	if (fpos < 0) {
		fpos = 0;
	}

	/* if class_name and func_name is OK */
	if (fpos > 2) {
		*class_name = estrndup(p, fpos-2);
	}
	*func_name = estrdup(&p[fpos]);

	return SUCCESS;
}

/* {{{ proto void pdl_watch_func(string $func[, callable $callback])
 */
PHP_FUNCTION(pdl_watch_func)
{
	zval	*cb = NULL;
	char	*func_str;
	uint	str_len;
	char	*class_name = NULL;
	char	*func_name = NULL;
	char	*handler_name = NULL;
	pdl_wf_t wf = {NULL, NULL, NULL};

	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|z!", &func_str, &str_len, &cb) == FAILURE) {
		return;
	}

	if (cb && !zend_is_callable(cb, 0, &handler_name TSRMLS_CC)) {
		php_error_docref(NULL TSRMLS_CC, E_NOTICE, "%s() is not a vaild callback function", handler_name);
		cb = NULL;
	}
	if (handler_name) {
		efree(handler_name);
	}

	if (pdl_parse_watch_func_name(func_str, str_len, &class_name, &func_name) == FAILURE) {
		php_error_docref(NULL TSRMLS_CC, E_WARNING, "%s is a illegal function name, right format: [class::]function", func_str);
		return;
	}

	wf.class_name = class_name;
	wf.func_name = func_name;
	if (cb) {
		Z_ADDREF_P(cb);
	}
	wf.watch_cb = cb;

	zend_str_tolower(func_str, str_len);
	zend_hash_update(&PDL_G(watch_funcs), func_str, str_len+1, &wf, sizeof(pdl_wf_t), NULL);

	/* HashTable will copy wf.watch_cb */
	if (cb) {
		zval_ptr_dtor(&cb);
	}
}
/* }}} */

/*
 * Local variables:
 * tab-width: 4
 * c-basic-offset: 4
 * End:
 * vim600: noet sw=4 ts=4 fdm=marker
 * vim<600: noet sw=4 ts=4
 */
